﻿#region Using Directives

using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using GeekRule.ORM.DataMapping;

#endregion

namespace GeekRule.ORM.Entities
{
    /// <summary>
    /// Creates instances of entities with property accessors wrapped to assign EntityState 
    /// in the EntityBase class.  It also creates a constructor that accepts an IDataReader 
    /// instance to assign data values during instance construction.
    /// </summary>
    public class EntityFactory
    {
        #region Private Members

        private static Dictionary<Type, Type> _entityWrapperCache = new Dictionary<Type, Type>();

        #endregion

        #region Public Methods

        /// <summary>
        /// Returns an instance of the dynamically created, derived entity type
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public static T GetInstance<T>() where T : EntityBase
        {
            return GetInstance<T>(null);
        }

        /// <summary>
        /// Return an instance of the dynamically created, derived entity type
        /// populated with values pulled from the reader
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="reader"></param>
        /// <returns></returns>
        public static T GetInstance<T>(IDataReader reader) where T : EntityBase
        {
            Type entityWrapperType;
            if (!_entityWrapperCache.ContainsKey(typeof (T)))
            {
                entityWrapperType = CreateEntityWrapperType(typeof (T));
                if (!_entityWrapperCache.ContainsKey(typeof(T)))
                {
                    _entityWrapperCache.Add(typeof (T), entityWrapperType);
                }
            }

            entityWrapperType = _entityWrapperCache[typeof (T)];

            if (reader == null)
            {
                return (T) Activator.CreateInstance(entityWrapperType);
            }
            else
            {
                return (T) Activator.CreateInstance(entityWrapperType, reader);
            }
        }

        #endregion

        #region Private Helpers

        /// <summary>
        /// Creates a dynamically derived type of <paramref name="entityType"/>.
        /// </summary>
        /// <param name="entityType"></param>
        /// <returns></returns>
        private static Type CreateEntityWrapperType(Type entityType)
        {
            // TODO: additional performance tuning
            // TODO: - Possible caching of Assembly/Builders
            // TODO: - Review for other possible areas

            AppDomain domain = AppDomain.CurrentDomain;
            AssemblyName assemblyName = new AssemblyName("GeekRule.ORM.Entities.Wrappers");
            AssemblyBuilder assemblyBuilder = domain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
            ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("GeekRule.ORM.Entities.Wrappers.Module");

            TypeBuilder wrapperTypeBuilder = moduleBuilder.DefineType(entityType.ToString() + "Wrapper", TypeAttributes.Public, entityType);

            PropertyInfo entityStateProperty = entityType.GetProperty("EntityState", typeof (EntityState));
            MethodInfo entityStateSetter = entityStateProperty.GetSetMethod();

            BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy;
            PropertyInfo[] properties = entityType.GetProperties();

            ConstructorInfo emptyConstructor = entityType.GetConstructor(Type.EmptyTypes);
            CreateEmptyConstructor(wrapperTypeBuilder, emptyConstructor, entityStateSetter);
            CreateReaderConstructor(wrapperTypeBuilder, emptyConstructor, entityStateSetter, properties);

            for (int i = 0; i < properties.Length; i++)
            {
                PropertyInfo property = properties[i];

                // Check mapping attribute
                bool mapProperty = true;
                object[] propertyAttributes = property.GetCustomAttributes(false);
                for (int j = 0; j < propertyAttributes.Length; j++)
                {
                    FieldMappingAttribute mappingAttribute = propertyAttributes[j] as FieldMappingAttribute;
                    if (mappingAttribute != null)
                    {
                        if (mappingAttribute.MappingMode == FieldMappingMode.Ignore)
                        {
                            mapProperty = false;
                            break;
                        }
                    }
                }

                if (!mapProperty)
                {
                    continue; // do not map this property
                }

                MethodInfo propertySetter = property.GetSetMethod();
                if (propertySetter != null && propertySetter.IsVirtual && (propertySetter.IsPublic || propertySetter.IsFamily))
                {
                    if (property.Name != "EntityState" && property.Name != "UnmappedFields")
                    {
                        CreatePropertyAccessorImplementation(property, wrapperTypeBuilder, entityStateSetter);
                    }
                }
            }

            return wrapperTypeBuilder.CreateType();
        }

        /// <summary>
        /// Creates an empty constructor for the entity wrapper class.
        /// </summary>
        /// <param name="typeBuilder"></param>
        /// <param name="constructorInfo"></param>
        /// <param name="entityStateSetter"></param>
        private static void CreateEmptyConstructor(TypeBuilder typeBuilder, ConstructorInfo constructorInfo, MethodInfo entityStateSetter)
        {
            ConstructorBuilder cb = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, Type.EmptyTypes);
            ILGenerator generator = cb.GetILGenerator();
            generator.Emit(OpCodes.Ldarg_0);
            generator.Emit(OpCodes.Call, constructorInfo);
            generator.Emit(OpCodes.Nop);
            generator.Emit(OpCodes.Nop);
            generator.Emit(OpCodes.Ldarg_0);
            generator.Emit(OpCodes.Ldc_I4_2);
            generator.Emit(OpCodes.Call, entityStateSetter);
            generator.Emit(OpCodes.Nop);
            generator.Emit(OpCodes.Nop);
            generator.Emit(OpCodes.Ret);
        }

        /// <summary>
        /// Creates a constructor acccepting an instance of IDataReader and creates property assignment
        /// operations for each property.
        /// </summary>
        /// <param name="typeBuilder"></param>
        /// <param name="constructorInfo"></param>
        /// <param name="entityStateSetter"></param>
        /// <param name="properties"></param>
        private static void CreateReaderConstructor(TypeBuilder typeBuilder, ConstructorInfo constructorInfo, MethodInfo entityStateSetter, PropertyInfo[] properties)
        {
            ConstructorBuilder cb = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new Type[] {typeof (IDataReader)});
            ILGenerator generator = cb.GetILGenerator();
            generator.Emit(OpCodes.Ldarg_0);
            generator.Emit(OpCodes.Call, constructorInfo);
            generator.Emit(OpCodes.Nop);
            generator.Emit(OpCodes.Nop);

            MethodInfo readerGetOrdinal = typeof (IDataRecord).GetMethod("GetOrdinal", new Type[] {typeof (String)});
            MethodInfo readerGetValue = typeof (IDataRecord).GetMethod("GetValue", new Type[] {typeof (Int32)});
            for (int i = 0; i < properties.Length; i++)
            {
                if (properties[i].Name == "EntityState" || properties[i].Name == "UnmappedFields")
                {
                    continue;
                }

                // Check mapping attribute
                bool mapProperty = true;
                object[] propertyAttributes = properties[i].GetCustomAttributes(false);
                for (int j = 0; j < propertyAttributes.Length; j++)
                {
                    FieldMappingAttribute mappingAttribute = propertyAttributes[j] as FieldMappingAttribute;
                    if (mappingAttribute != null)
                    {
                        if (mappingAttribute.MappingMode == FieldMappingMode.Ignore)
                        {
                            mapProperty = false;
                            break;
                        }
                    }
                }

                if (!mapProperty)
                {
                    continue; // do not map this property
                }

                CreateValueAssignmentImplementation(generator, properties[i], readerGetOrdinal, readerGetValue);
            }

            generator.Emit(OpCodes.Ldarg_0);
            generator.Emit(OpCodes.Ldc_I4_S, 16);
            generator.Emit(OpCodes.Call, entityStateSetter);
            generator.Emit(OpCodes.Nop);
            generator.Emit(OpCodes.Nop);
            generator.Emit(OpCodes.Ret);
        }

        /// <summary>
        /// Creates the value assignment logic
        /// --------------------------
        /// Reference type property such as string:
        ///   base.StringProperty = DataTypeConverter.GetRefTypeValue&lt;string&gt;(reader, "StringColumn");
        /// 
        /// Value type property such as int:
        ///   base.IntProperty = DataTypeConverter.GetValueTypeValue&lt;int&gt;(reader, "IntColumn");
        /// </summary>
        /// <param name="generator"></param>
        /// <param name="property"></param>
        /// <param name="readerGetOrdinal"></param>
        /// <param name="readerGetValue"></param>
        private static void CreateValueAssignmentImplementation(ILGenerator generator, PropertyInfo property, MethodInfo readerGetOrdinal, MethodInfo readerGetValue)
        {
            MethodInfo setterMethod = property.GetSetMethod();

            bool isValueType = property.PropertyType.IsValueType;

            generator.Emit(OpCodes.Ldarg_0);
            generator.Emit(OpCodes.Ldarg_1);
            generator.Emit(OpCodes.Ldstr, property.Name);

            if (isValueType)
            {
                MethodInfo converterMethod = typeof (DataTypeConverter).GetMethod("GetValueTypeValue");

                if (converterMethod != null)
                {
                    MethodInfo genericMethod = converterMethod.MakeGenericMethod(property.PropertyType);
                    generator.EmitCall(OpCodes.Call, genericMethod, new Type[] {property.PropertyType});
                }
                else
                {
                    throw new NotImplementedException("Converter not implemented for " + property.PropertyType);
                }
            }
            else
            {
                MethodInfo converterMethod = typeof (DataTypeConverter).GetMethod("GetRefTypeValue");

                if (converterMethod != null)
                {
                    MethodInfo genericMethod = converterMethod.MakeGenericMethod(property.PropertyType);
                    generator.EmitCall(OpCodes.Call, genericMethod, new Type[] {property.PropertyType});
                }
                else
                {
                    throw new NotImplementedException("Converter not implemented for " + property.PropertyType);
                }
            }

            generator.Emit(OpCodes.Call, setterMethod);
            generator.Emit(OpCodes.Nop);
        }

        /// <summary>
        /// Creates an override for the specified property in the base class.
        /// Properties look like:
        /// --------------------------
        /// object MyProperty
        /// {
        ///     get { return base.MyProperty; }
        ///     set
        ///     {
        ///         if (base.MyProperty != value)
        ///         {
        ///             EntityState = EntityState.Dirty;
        ///         }
        ///         base.MyProperty = value;
        ///     }
        /// }
        /// --------------------------
        /// </summary>
        /// <param name="item"></param>
        /// <param name="typeBuilder"></param>
        /// <param name="entityStateMethod"></param>
        private static void CreatePropertyAccessorImplementation(PropertyInfo item, TypeBuilder typeBuilder, MethodInfo entityStateMethod)
        {
            // TODO: check additional data types since this code may need tweaks beyond value-type vs. non-value-type.

            MethodInfo setMethod = item.GetSetMethod();
            MethodInfo getMethod = item.GetGetMethod();

            //get an array of the parameter types.
            var types = from t in setMethod.GetParameters()
                        select t.ParameterType;

            MethodBuilder setMethodBuilder = typeBuilder.DefineMethod(setMethod.Name, setMethod.Attributes, setMethod.ReturnType, types.ToArray());
            typeBuilder.DefineMethodOverride(setMethodBuilder, setMethod);
            ILGenerator setMethodWrapperIl = setMethodBuilder.GetILGenerator();

            // Get existing value/reference
            setMethodWrapperIl.DeclareLocal(typeof (bool));
            setMethodWrapperIl.Emit(OpCodes.Nop);
            setMethodWrapperIl.Emit(OpCodes.Ldarg_0);
            setMethodWrapperIl.EmitCall(OpCodes.Call, getMethod, null);
            setMethodWrapperIl.Emit(OpCodes.Ldarg_1);

            // If this is a reference type, compare with op_Inequality(a, b)
            if (!item.PropertyType.IsValueType && !(item.PropertyType.IsArray && item.PropertyType.GetElementType().IsValueType) && (item.PropertyType != typeof (DBNull)))
            {
                MethodInfo opInequalityMI = GetOpInequalityMethod(item.PropertyType);
                setMethodWrapperIl.EmitCall(OpCodes.Call, opInequalityMI, new Type[] {item.PropertyType, item.PropertyType});
                setMethodWrapperIl.Emit(OpCodes.Ldc_I4_0);
            }

            // Check if incoming value matches existing stored value/reference
            setMethodWrapperIl.Emit(OpCodes.Ceq);
            setMethodWrapperIl.Emit(OpCodes.Stloc_0);
            setMethodWrapperIl.Emit(OpCodes.Ldloc_0);
            Label setValueLabel = setMethodWrapperIl.DefineLabel();
            setMethodWrapperIl.Emit(OpCodes.Brtrue_S, setValueLabel);

            // Set EntityState.Dirty
            setMethodWrapperIl.Emit(OpCodes.Nop);
            setMethodWrapperIl.Emit(OpCodes.Ldarg_0);
            setMethodWrapperIl.Emit(OpCodes.Ldc_I4_4);
            setMethodWrapperIl.EmitCall(OpCodes.Call, entityStateMethod, new Type[] {typeof (EntityState)});
            setMethodWrapperIl.Emit(OpCodes.Nop);
            setMethodWrapperIl.Emit(OpCodes.Nop);

            // Call base property setter and return
            setMethodWrapperIl.MarkLabel(setValueLabel);
            setMethodWrapperIl.Emit(OpCodes.Ldarg_0);
            setMethodWrapperIl.Emit(OpCodes.Ldarg_1);
            setMethodWrapperIl.EmitCall(OpCodes.Call, setMethod, new Type[] {item.PropertyType});
            setMethodWrapperIl.Emit(OpCodes.Nop);
            setMethodWrapperIl.Emit(OpCodes.Ret);
        }

        /// <summary>
        /// Get the op_Inequality method for the given type used to compare non-value type types.
        /// </summary>
        /// <param name="type"></param>
        /// <returns>Returns the MethodInfo if it exists, null otherwise</returns>
        private static MethodInfo GetOpInequalityMethod(Type type)
        {
            MethodInfo opInequality = type.GetMethod("op_Inequality");

            if (opInequality == null && type.BaseType != typeof (object))
            {
                return GetOpInequalityMethod(type.BaseType);
            }

            return opInequality;
        }

        #endregion
    }
}
